<template>
  <div
    v-move="onMove"
    ref="circleSilderRef"
    class="_circle"
    :class="{ _mobile: isMobile }"
  >
    <!-- 旋转指针 -->
    <div
      ref="spinRef"
      class="_pointer-wrapper"
      :style="spinStyle"
      @transitionend="onTransitionEnd"
    >
      <img class="_point" :src="boxActibeArrow" />
    </div>

    <svg
      class="_circle-wrapper"
      :width="progressParams.svgSize"
      :height="progressParams.svgSize"
      :viewBox="`0 0 ${progressParams.svgSize} ${progressParams.svgSize}`"
    >
      <circle
        class="_progress-base-circle"
        :r="progressParams.circleRadius"
        :stroke-width="props.circleWidth"
        :stroke="props.sliderColor"
      />
      <path
        class="_progress-circle"
        :d="path"
        :stroke-width="props.circleWidth"
      />

      <g class="_progress-start">
        <circle
          :r="sliderBar.outer"
          :cx="points.start.x"
          :cy="points.start.y"
          fill="#FFF"
        />
        <circle
          :r="sliderBar.innter"
          :cx="points.start.x"
          :cy="points.start.y"
          fill="#17DB05"
        />
      </g>

      <g class="_progress-end">
        <circle
          :r="sliderBar.outer"
          :cx="points.end.x"
          :cy="points.end.y"
          fill="#FFF"
        />
        <circle
          :r="sliderBar.innter"
          :cx="points.end.x"
          :cy="points.end.y"
          fill="#17DB05"
        />
      </g>
    </svg>
  </div>
</template>

<script lang="tsx" setup>
import type { CSSProperties } from "vue";

import boxActibeArrow from "@/assets/box-active-arrow.png";
import { useMedia } from "@/hooks/useMedia";

import type { CircleSliderExpose } from "./circleSlider";
import { circleSliderEmits, circleSliderProps } from "./circleSlider";
import { BindType, useMove } from "./useMove";

const props = defineProps(circleSliderProps);
const emits = defineEmits(circleSliderEmits);

const { vMove } = useMove();
const isMobile = useMedia();

const spinRef = ref<HTMLDivElement>();
const circleSilderRef = ref<HTMLDivElement>();
const progressParams = reactive({
  sliderBar: {
    outer: 8,
    innter: -2,
  },
  baseAngle: -90,
  svgSize: 0,
  circleRadius: 0,
  center: [0, 0],
  animationMoveAngle: 0,
  startOffsetProgress: 0,
  endOffsetProgress: 0,
});

const spinAnimation = ref({
  opacity: 0,
  targetAngle: 0,
  callback: void 0,
  targetProcess: 0,
});

const spinStyle = computed(() => {
  const styles: CSSProperties = {
    opacity: spinAnimation.value.opacity,
    transform: `rotate(${spinAnimation.value.targetAngle}deg)`,
  };
  if (spinAnimation.value.targetAngle > 360) {
    styles.transition = "transform 6s cubic-bezier(0.15, 0.05, 0.05, 1)";
  }
  return styles;
});

function onTransitionEnd() {
  spinAnimation.value?.callback?.();
  spinAnimation.value.targetAngle = spinAnimation.value.targetAngle % 360;
  emits("onAnimationEnd");
}

const sliderBar = computed(() => {
  const { outer, innter } = progressParams.sliderBar;
  return {
    outer: props.circleWidth / 2 + outer,
    innter: props.circleWidth / 2 + innter,
  };
});

const innerProgress = computed(() => {
  const { startOffsetProgress, endOffsetProgress } = progressParams;
  return props.progress - startOffsetProgress - endOffsetProgress;
});

const offsetAngle = computed(() => {
  const { startOffsetProgress, endOffsetProgress } = progressParams;
  return {
    start: startOffsetProgress * 360,
    end: endOffsetProgress * 360,
  };
});

const points = computed(() => {
  const baseAngle = -90;
  const progressAngleHalf = (innerProgress.value / 2) * 360;
  const startAngle =
    (baseAngle +
      progressParams.animationMoveAngle -
      progressAngleHalf -
      offsetAngle.value.start) %
    360;
  const endAngle =
    (baseAngle +
      progressParams.animationMoveAngle +
      progressAngleHalf +
      offsetAngle.value.end) %
    360;
  const startPoint = getArcPoint(startAngle);
  const endPoint = getArcPoint(endAngle);
  return {
    start: {
      angle: startAngle - baseAngle,
      x: startPoint[0],
      y: startPoint[1],
    },
    end: {
      angle: endAngle - baseAngle,
      x: endPoint[0],
      y: endPoint[1],
    },
  };
});

const path = computed(() => {
  const { start, end } = points.value;
  const radiusType = props.progress >= 0.5 ? "1" : "0";
  return `
    M ${start.x} ${start.y}
    A ${progressParams.circleRadius} ${progressParams.circleRadius} 0 ${radiusType} 1 ${end.x} ${end.y}
  `;
});

function getArcPoint(angle: number) {
  const [x, y] = progressParams.center;
  const circleRadius = progressParams.circleRadius;
  return [
    x + circleRadius * Math.cos((angle * Math.PI) / 180),
    y + circleRadius * Math.sin((angle * Math.PI) / 180),
  ];
}

function calculateAngle(x1, y1) {
  const x2 = progressParams.center[0];
  const y2 = progressParams.center[1];
  const x3 = progressParams.center[0];
  const y3 = 0;
  const BA = { x: x1 - x2, y: y1 - y2 };
  const BC = { x: x3 - x2, y: y3 - y2 };
  const dotProduct = BA.x * BC.x + BA.y * BC.y;
  const magnitudeBA = Math.sqrt(BA.x * BA.x + BA.y * BA.y);
  const magnitudeBC = Math.sqrt(BC.x * BC.x + BC.y * BC.y);
  const cosTheta = dotProduct / (magnitudeBA * magnitudeBC);
  const thetaRad = Math.acos(cosTheta);
  const thetaDeg = thetaRad * (180 / Math.PI);
  return Math.abs(x1 < progressParams.center[1] ? 360 - thetaDeg : thetaDeg);
}

function getStartAndEndAngle(start, end) {
  if (!isMobile.value) {
    return {
      startAngle: calculateAngle(start.offsetX, start.offsetY),
      endAngle: calculateAngle(end.offsetX, end.offsetY),
    };
  }
  if (end.touches.length === 0) {
    end = start;
  }
  return {
    startAngle: calculateAngle(
      start.touches?.[0]?.pageX,
      start.touches?.[0]?.pageY
    ),
    endAngle: calculateAngle(end.touches?.[0]?.pageX, end.touches?.[0]?.pageY),
  };
}

function onMove({ start, end, type }) {
  const { startAngle, endAngle } = getStartAndEndAngle(start, end);
  const moveDiffAngle =
    type === BindType.Start ? startAngle - endAngle : endAngle - startAngle;

  if (Math.abs(moveDiffAngle) > 10) {
    return;
  }

  if (type === BindType.Path) {
    progressParams.animationMoveAngle =
      (progressParams.animationMoveAngle + moveDiffAngle) % 360;
    return;
  }

  let diffProgress = moveDiffAngle / 360;
  let targetProgress = props.progress + diffProgress;
  targetProgress = Math.min(targetProgress, props.max);
  targetProgress = Math.max(targetProgress, props.min);
  diffProgress = targetProgress - props.progress;
  emits("update:progress", targetProgress);
  if (type === BindType.Start) {
    progressParams.startOffsetProgress += diffProgress;
  }

  if (type === BindType.End) {
    progressParams.endOffsetProgress += diffProgress;
  }
}

function getCircleRadius() {
  const { offsetWidth, offsetHeight } = circleSilderRef.value;
  const length = Math.min(offsetHeight, offsetWidth);
  const radiusOffset = isMobile.value ? 30 : 20;
  progressParams.svgSize = length;
  progressParams.circleRadius = length / 2 - radiusOffset;
  progressParams.center = [length / 2, length / 2];
}

onMounted(() => {
  getCircleRadius();
});

defineExpose<CircleSliderExpose>({
  startSpin: async (targetProgress, cb) => {
    emits("onAnimationStart");
    await nextTick();
    spinAnimation.value.targetProcess = targetProgress;
    spinAnimation.value.opacity = 1;
    spinAnimation.value.callback = cb;
    spinAnimation.value.targetAngle =
      targetProgress * 360 + points.value.start.angle + 4 * 360;
  },
});
</script>

<style lang="less" scoped>
._circle {
  @apply relative flex items-center justify-center w-full h-full;
  padding: 0.88rem;
  ._circle-wrapper {
    position: relative;
    z-index: 1;
    width: 100%;
    height: 100%;
    ._progress-base-circle {
      cx: 50%;
      cy: 50%;
      fill: none;
    }

    ._progress-circle {
      stroke: var(--el-color-primary);
      cursor: grab;
      fill: none;
    }
  }

  ._progress-start,
  ._progress-end {
    cursor: grab;
  }

  &._mobile {
    padding: 0;
  }
}

._pointer-wrapper {
  opacity: 0;
  position: absolute;
  z-index: 0;
  top: 0;
  left: 0;
  width: 100%;
  height: 50%;
  transform-origin: 50% 100% 0;
  transform: rotate(0deg);
  user-select: none;
  ._point {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 0.88rem;
  }
}
</style>
